package com.smartalarmclock.Model;

import java.util.HashSet;
import java.util.Hashtable;
import java.util.Set;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import com.smartalarmclock.Model.Reference.DeleteMethod;
import com.smartalarmclock.Model.Exceptions.DataEntityDoesNotExistException;
import com.smartalarmclock.Model.Exceptions.InvalidDeleteDataEntityOperationException;
import com.smartalarmclock.Model.Exceptions.InvalidNullReferenceOnDeleteException;
import com.smartalarmclock.Model.Exceptions.InvalidReferenceToRemoveException;
import com.smartalarmclock.Model.Exceptions.InvalidSaveDataEntityOperationException;
import com.smartalarmclock.Model.Exceptions.ReferenceCannotBeNullException;

abstract class RulesItem extends ReferencedDataEntity implements IRulesItem {

	// Database Constants
	
	public static final String RulesItemTableName = "RulesItem";
	private static final String c_RulesItemColumnNameAlarmClock = "AlarmClock";
	private static final String c_RulesItemColumnNameMatchingAutoResponse = "MatchingAutoResponse";
	private static final String[] c_QueryAllRulesItemColumns = new String[] { c_ColumnNameId, c_RulesItemColumnNameAlarmClock, c_RulesItemColumnNameMatchingAutoResponse };
	
	protected static final String RulesItemToTimeFrames = "RulesItemToTimeFrames";
	protected static final String c_RulesItemToTimeFramesColumnNameRulesItem = "RulesItem";
	protected static final String c_RulesItemToTimeFramesColumnNameTimeFrame = "TimeFrame";
	protected static final String[] c_QueryAllRulesItemToTimeFramesColumns = new String[] {
		c_RulesItemToTimeFramesColumnNameRulesItem, c_RulesItemToTimeFramesColumnNameTimeFrame };
	
	// Members
	
	protected final SingleReference<IAlarmClock> m_AlarmClockReference;
	protected final SingleReference<IAutoResponse> m_MatchingAutoResponseReference;
	protected final MultipleReference<ITimeFrame> m_TimeFrameReferences;
	
	// Constructor
	
	/**
	 * Constructor
	 * @param alarmClock the alarm clock to reference
	 * @param matchingAutoResponse the auto response for the matching contacts
	 */
	protected RulesItem(IAlarmClock alarmClock, IAutoResponse matchingAutoResponse) {
		super();
		m_AlarmClockReference = new SingleReference<IAlarmClock>(this, alarmClock, false, AlarmClock.class,	DeleteMethod.Delete);	
		m_MatchingAutoResponseReference = new SingleReference<IAutoResponse>(
			this, matchingAutoResponse, false, AutoResponse.class,	DeleteMethod.Delete);
		m_TimeFrameReferences = new MultipleReference<ITimeFrame>(this, TimeFrame.class, DeleteMethod.Delete);
	}
	
	/**
	 * Constructor
	 * @param id the id of the entity
	 * @param alarmClock the alarm clock to reference
	 * @param matchingAutoResponse the auto response for the matching contacts
	 */
	protected RulesItem(int id, IAlarmClock alarmClock, IAutoResponse matchingAutoResponse) {
		super(id);
		m_AlarmClockReference = new SingleReference<IAlarmClock>(this, alarmClock, false, AlarmClock.class,	DeleteMethod.Delete);	
		m_MatchingAutoResponseReference = new SingleReference<IAutoResponse>(
			this, matchingAutoResponse, false, AutoResponse.class,	DeleteMethod.Delete);
		m_TimeFrameReferences = new MultipleReference<ITimeFrame>(this, TimeFrame.class, DeleteMethod.Delete);
	}
	
	// IRulesItem implementation

	/**
	 * @return the rules item's alarm clock
	 */
	@Override
	public IAlarmClock GetAlarmClock() {
		return m_AlarmClockReference.GetReference();
	}

	/**
	 * @return the rules item's auto response for the contacts in the rules item
	 */
	@Override
	public IAutoResponse GetMatchingAutoResponse() {
		return m_MatchingAutoResponseReference.GetReference();
	}
	
	/**
	 * @param context the context of the request
	 * @return a set of the rules item's time frames
	 */
	@Override
	public Set<ITimeFrame> GetTimeFrames(Context context) {
		return m_TimeFrameReferences.GetReferences(context);
	}
	
	/**
	 * Adds a time fame to the rules item
	 * @param context the context of the request
	 * @param timeFrame the time frame to add
	 */
	@Override
	public void AddTimeFrame(Context context, ITimeFrame timeFrame) {
		m_TimeFrameReferences.Add(context, timeFrame);
		SetUpdated();
	}
	
	/**
	 * Removes a time frame from the rules item
	 * @param context the context of the request
	 * @param timeFrame the time frame to remove
	 */
	public void RemoveTimeFrame(Context context, ITimeFrame timeFrame) {
		m_TimeFrameReferences.Remove(context, timeFrame);
		SetUpdated();
	}
	
	// Public Methods
	
	/**
	 * @return the CREATE TABLE statement of the RulesItem table
	 */
	public static String GetSqlStatementCreateRulesItemTable() {
		return "CREATE TABLE " + RulesItemTableName + " (" +
				c_ColumnNameId + 							" INTEGER PRIMARY KEY, " +
				c_RulesItemColumnNameAlarmClock + 			" INTEGER NOT NULL, " +
				c_RulesItemColumnNameMatchingAutoResponse + " INTEGER UNIQUE NOT NULL, " +
				"FOREIGN KEY(" + c_RulesItemColumnNameAlarmClock + ") REFERENCES " + AlarmClock.AlarmClockTableName + "(" + c_ColumnNameId + ")," +
				"FOREIGN KEY(" + c_RulesItemColumnNameMatchingAutoResponse + ") REFERENCES " + AutoResponse.AutoResponseTableName + "(" + c_ColumnNameId + "))";
	}
	
	/**
	 * @return the DROP TABLE statement of the RulesItem table
	 */
	public static String GetSqlStatementDropRulesItemTable() {
		return "DROP TABLE IF EXISTS " + RulesItemTableName;
	}
	
	/**
	 * @return the CREATE TABLE statement of the RulesItemToTimeFrames table
	 */
	public static String GetSqlStatementCreateRulesItemToTimeFramesTable() {
		return "CREATE TABLE " + RulesItemToTimeFrames + " (" +
				c_RulesItemToTimeFramesColumnNameRulesItem + " INTEGER NOT NULL, " +
				c_RulesItemToTimeFramesColumnNameTimeFrame + " INTEGER NOT NULL, " +
				"PRIMARY KEY(" + c_RulesItemToTimeFramesColumnNameRulesItem + ", " + c_RulesItemToTimeFramesColumnNameTimeFrame + ")," +
				"FOREIGN KEY(" + c_RulesItemToTimeFramesColumnNameRulesItem + ") REFERENCES " + RulesItem.RulesItemTableName + "(" + c_ColumnNameId + ")," +
				"FOREIGN KEY(" + c_RulesItemToTimeFramesColumnNameTimeFrame + ") REFERENCES " + TimeFrame.TimeFrameTableName + "(" + c_ColumnNameId + "))";
	}
	
	/**
	 * @return the DROP TABLE statement of the RulesItemToTimeFrames table
	 */
	public static String GetSqlStatementDropRulesItemToTimeFramesTable() {
		return "DROP TABLE IF EXISTS " + RulesItemToTimeFrames;
	}
	
	// Protected Methods
	
	/**
	 * Queries the DB for a rules item with the given id
	 * @param db a readable database
	 * @param id the id to query for
	 * @return a cursor with the matching values.
	 */
	protected static Cursor GetRulesItemValuesById(SQLiteDatabase db, int id) {
		return db.query(
			RulesItemTableName,
			c_QueryAllRulesItemColumns,
			c_ColumnNameId + "=" + id,
			null, null, null, null);
	}
	
	/**
	 * Queries the DB for a rules item with the given alarm clock
	 * @param db a readable database
	 * @param alarmClock the alarm clock to query for
	 * @return a cursor with the matching values.
	 */
	protected static Cursor GetRulesItemValuesByAlarmClock(SQLiteDatabase db, IAlarmClock alarmClock) {
		return db.query(
			RulesItemTableName,
			c_QueryAllRulesItemColumns,
			c_RulesItemColumnNameAlarmClock + "=" + alarmClock.GetId(),
			null, null, null, null);
	}
	
	/**
	 * Queries the DB for a rules item with the given auto response
	 * @param db a readable database
	 * @param matchingAutoResponse the auto response to query for
	 * @return a cursor with the matching values.
	 */
	protected static Cursor GetRulesItemValuesByMatchingAutoResponse(SQLiteDatabase db, IAutoResponse matchingAutoResponse) {
		return db.query(
			RulesItemTableName,
			c_QueryAllRulesItemColumns,
			c_RulesItemColumnNameMatchingAutoResponse + "=" + matchingAutoResponse.GetId(),
			null, null, null, null);
	}
	
	/**
	 * Queries the RulesItem with the given TimeFrame
	 * @param context the context of the request
	 * @param db a readable DB
	 * @param timeFrame the time frame to query
	 * @return a cursor with the matching values
	 * @throws DataEntityDoesNotExistException if the rules item does not exist
	 */
	protected static Cursor GetRulesItemValuesByTimeFrame(Context context, SQLiteDatabase db, ITimeFrame timeFrame) throws DataEntityDoesNotExistException {
		int rulesItemId = GetRulesItemIdByTimeFrame(context, timeFrame);
		return GetRulesItemValuesById(db, rulesItemId);
	}
	
	/**
	 * Queries the RulesItem id with the given TimeFrame
	 * @param context the context of the request
	 * @param timeFrame the time frame to query
	 * @return the matching rules item id
	 * @throws DataEntityDoesNotExistException if the rules item does not exist
	 */
	protected static int GetRulesItemIdByTimeFrame(Context context, ITimeFrame timeFrame) throws DataEntityDoesNotExistException {
		SQLiteDatabase db = OpenDBForReading(context);
		Cursor cursor = db.query(
			RulesItemToTimeFrames,
			c_QueryAllRulesItemToTimeFramesColumns,
			c_RulesItemToTimeFramesColumnNameTimeFrame + "=" + timeFrame.GetId(),
			null, null, null, null);
		
		if (!cursor.moveToNext()) {
			throw new DataEntityDoesNotExistException(
				RulesItem.class, String.format("containing the TimeFrame with the id %d", timeFrame.GetId()));
		}
		
		try {
			return cursor.getInt(cursor.getColumnIndex(c_RulesItemToTimeFramesColumnNameRulesItem));
		}
		finally {
			cursor.close();
			db.close();
		}
	}
	
	/**
	 * Retrieves the alarm clock instance from the cursor containing a rules item from the database.
	 * The cursor must be set to the record.
	 * @param context the context of the request
	 * @param cursor the cursor containing the rules item values.
	 * @return the matching alarm clock instance
	 * @throws DataEntityDoesNotExistException if the alarm clock does not exist
	 */
	protected static IAlarmClock GetAlarmClockFromCursor(Context context, Cursor cursor) throws DataEntityDoesNotExistException {
		int alarmClockId = cursor.getInt(cursor.getColumnIndex(c_RulesItemColumnNameAlarmClock));
		return new AlarmClockFactory().GetAlarmClockById(context, alarmClockId);
	}
	
	/**
	 * Retrieves the matching auto response instance from the cursor containing a rules item from the database.
	 * The cursor must be set to the record.
	 * @param context the context of the request
	 * @param cursor the cursor containing the rules item values.
	 * @return the matching auto response instance
	 * @throws DataEntityDoesNotExistException if the auto response does not exist
	 */
	protected static IAutoResponse GetMatchingAutoResponseFromCursor(Context context, Cursor cursor) throws DataEntityDoesNotExistException {
		int autoResponseId = cursor.getInt(cursor.getColumnIndex(c_RulesItemColumnNameMatchingAutoResponse));
		return new AutoResponseFactory().GetAutoResponseById(context, autoResponseId);
	}
	
	// Protected Methods
	
	/**
	 * Refreshes the members using a copy of the rules item queries from the DB
	 * @param context the context of the request
	 * @param copyRulesItem the copy of the rules item to use
	 */
	protected void Refresh(Context context, IRulesItem copyRulesItem) {
		try {
			m_AlarmClockReference.Set(copyRulesItem.GetAlarmClock());
			m_MatchingAutoResponseReference.Set(copyRulesItem.GetMatchingAutoResponse());
		} catch (ReferenceCannotBeNullException e) { }
		
		m_TimeFrameReferences.Refresh(context);
	}
	
	// IDataEntity implementation
	
	@Override
	public void Save(Context context) throws InvalidSaveDataEntityOperationException {
		if (IsUpdated()) {
			// Saving the referenced objects before adding them to the profile in the DB.
			m_AlarmClockReference.SaveReferences(context);
			m_MatchingAutoResponseReference.SaveReferences(context);
						
			super.Save(context);
			
			m_TimeFrameReferences.SaveReferences(context);
		}
	}
	
	@Override
	public void Delete(Context context)	throws InvalidDeleteDataEntityOperationException, InvalidSaveDataEntityOperationException {
		m_TimeFrameReferences.OnDeleteReferencingEntity(context);
		
		super.Delete(context);
		
		// Deleting references if needed.
		m_AlarmClockReference.OnDeleteReferencingEntity(context);
		m_MatchingAutoResponseReference.OnDeleteReferencingEntity(context);
	}
	
	// DataEntity implementation
	
	@Override
	protected Hashtable<String, ContentValues> GetRecordValues() {
		ContentValues values = new ContentValues();
		
		values.put(c_RulesItemColumnNameAlarmClock, m_AlarmClockReference.GetReference().GetId());
		values.put(c_RulesItemColumnNameMatchingAutoResponse, m_MatchingAutoResponseReference.GetReference().GetId());
		
		Hashtable<String, ContentValues> valuesTable = new Hashtable<String, ContentValues>();
		valuesTable.put(RulesItemTableName, values);
		return valuesTable;
	}
	
	@Override
	protected Set<String> GetTableNames() {
		Set<String> tableNames = new HashSet<String>();
		tableNames.add(RulesItemTableName);
		return tableNames;
	}
	
	// IReferencingMultipleDataEnity implementation
	
	@Override
	public boolean IsReferenced(Context context, IReferencedDataEntity referencedEntity) {
		return referencedEntity == m_AlarmClockReference.GetReference() ||
			referencedEntity == m_MatchingAutoResponseReference.GetReference() ||
			m_TimeFrameReferences.GetReferences(context).contains(referencedEntity);
	}

	@Override
	public boolean CanRemoveReference(Context context, IReferencedDataEntity referencedEntity) {
		if (m_TimeFrameReferences.GetReferences(context).contains(referencedEntity)) {
			// We can remove time frame from rules items
			return true;
		}
		
		// The alarm clock and matching auto response are not nullable so we cannot remove them.
		// And if the entity is not referenced at all we cannot remove it.
		return false;
	}

	@Override
	public void RemoveReference(Context context, IReferencedDataEntity referencedEntity) throws InvalidNullReferenceOnDeleteException, InvalidReferenceToRemoveException {
		if (!IsReferenced(context, referencedEntity)) {
			throw new InvalidReferenceToRemoveException(getClass(), GetId(), referencedEntity.getClass(), referencedEntity.GetId());
		}
		
		if (!CanRemoveReference(context, referencedEntity)) {
			throw new InvalidNullReferenceOnDeleteException(getClass(), GetId(), referencedEntity.getClass(), referencedEntity.GetId());
		}
		
		if (ITimeFrame.class.isInstance(referencedEntity)) {
			RemoveTimeFrame(context, (ITimeFrame)referencedEntity);
		}
	}
	
	@Override
	public void LoadReferences(Context context, Class<?> referencedType) {
		if (TimeFrame.class == referencedType) {
			SQLiteDatabase db = OpenDBForReading(context);
			Cursor cursor = db.query(
				RulesItemToTimeFrames,
				c_QueryAllRulesItemToTimeFramesColumns,
				c_RulesItemToTimeFramesColumnNameRulesItem + "=" + GetId(),
				null, null, null, null);
			
			int timeFrameId = 0;
			ITimeFrame timeFrame = null;
			TimeFrameFactory factory = new TimeFrameFactory();
			
			while (cursor.moveToNext()) {
				timeFrameId = cursor.getInt(cursor.getColumnIndex(c_RulesItemToTimeFramesColumnNameTimeFrame));
				timeFrame = factory.TryGetTimeFrameById(context, timeFrameId);
				m_TimeFrameReferences.Add(context, timeFrame);
			}
			
			cursor.close();
			db.close();
		}
	}
	
	@Override
	public void SaveReferenceConnection(Context context, IReferencedDataEntity referencedEntity) {
		if (ITimeFrame.class.isInstance(referencedEntity)) {
			SQLiteDatabase db = OpenDBForWriting(context);
			
			// Checking if the connection exists
			Cursor cursor = db.query(
				RulesItemToTimeFrames,
				c_QueryAllRulesItemToTimeFramesColumns,
				c_RulesItemToTimeFramesColumnNameRulesItem + "=" + GetId() + " AND " + c_RulesItemToTimeFramesColumnNameTimeFrame + "=" + referencedEntity.GetId(),
				null, null, null, null);
			
			if (!cursor.moveToNext()) {
				// If no records were queried, add the connection between the contact and the group
				ContentValues values = new ContentValues();
				values.put(c_RulesItemToTimeFramesColumnNameRulesItem, GetId());
				values.put(c_RulesItemToTimeFramesColumnNameTimeFrame, referencedEntity.GetId());
				
				db.insert(RulesItemToTimeFrames, null, values);
			}
			
			cursor.close();
			db.close();
		}
	}
	
	@Override
	public void DeleteReferenceConnection(Context context, IReferencedDataEntity referencedEntity) {
		if (ITimeFrame.class.isInstance(referencedEntity)) {
			SQLiteDatabase db = OpenDBForWriting(context);
			
			// Checking if the connection exists
			Cursor cursor = db.query(
				RulesItemToTimeFrames,
				c_QueryAllRulesItemToTimeFramesColumns,
				c_RulesItemToTimeFramesColumnNameRulesItem + "=" + GetId() + " AND " + c_RulesItemToTimeFramesColumnNameTimeFrame + "=" + referencedEntity.GetId(),
				null, null, null, null);
			
			if (cursor.moveToNext()) {
				db.delete(RulesItemToTimeFrames,
					c_RulesItemToTimeFramesColumnNameRulesItem + "=" + GetId() + " AND " + c_RulesItemToTimeFramesColumnNameTimeFrame + "=" + referencedEntity.GetId(),
					null); 
			}
			
			cursor.close();
			db.close();
		}
	}
}
